package biz.datalk.industrialland.common.advice;

import biz.datalk.commons.utils.BasicParserUtils;
import biz.datalk.commons.utils.json.JsonUtil;
import biz.datalk.industrialland.common.advice.cfg.DatalkCryptoCfgs;
import biz.datalk.industrialland.common.component.AppEncryptSecurityKeyComponent;
import biz.datalk.industrialland.common.def.CommonConstant;
import biz.datalk.industrialland.common.encrypt.AESTool;
import biz.datalk.industrialland.common.encrypt.RSATool;
import biz.datalk.industrialland.common.encrypt.annotation.RSAES;
import biz.datalk.industrialland.common.encrypt.sm.GMUtils;
import biz.datalk.industrialland.common.exception.ApplicationException;
import biz.datalk.industrialland.common.result.UnifyResult;
import biz.datalk.industrialland.common.result.UnifyResultCode;
import biz.datalk.industrialland.common.util.*;
import com.google.common.collect.Maps;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * 校核签名的拦截器
 *
 * @author tarofang@163.com
 * @date 2019-11-10
 */
// @Component("checkSignatureInterceptor")
@Slf4j
public class CheckSignatureInterceptor implements HandlerInterceptor {

    private final RedisUtil redisUtil;
    private final DatalkCryptoCfgs datalkCryptoCfgs;

    @Autowired
    public CheckSignatureInterceptor(RedisUtil redisUtil, DatalkCryptoCfgs datalkCryptoCfgs) {
        this.redisUtil = redisUtil;
        this.datalkCryptoCfgs = datalkCryptoCfgs;
    }

    private final String reqSignTimestampTimeoutKey = "req.sign.timestamp.timeout";
    private final long reqSignTimestampTimeoutDefaultSecond = 10 * 60L;
    private final String reqSignNonceTimeoutKey = "req.sign.nonce.timeout";
    private final long reqSignNonceTimeoutDefaultSecond = 10 * 60L;
    private String reqSignNoncePrefix = "req_sign_check:";

    public static final String PARAM_SIGNATURE = "signature";

    @Autowired
    private AppEncryptSecurityKeyComponent appEncryptSecurityKeyComponent;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String reqMethod = StringUtils.trimToEmpty(request.getMethod());
        log.debug("{}:{}", reqMethod, request.getRequestURL());

        // 统一加解密 必然存在 HandlerMethod
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        if (!method.isAnnotationPresent(RSAES.class)) {
            return true;
        }

        // String contextPath = request.getContextPath();
        // log.warn(request.getRequestURI());

        HttpMethod hm = HttpMethod.resolve(reqMethod);
        if (hm == null) {
            log.warn("Invalid Http Method: {}", reqMethod);
            throw new ApplicationException(UnifyResultCode.METHOD_NO_SUPPORT_ERROR, I18NUtil.getMsg("method.no.support.error", reqMethod));
        }

        switch (hm) {
            case OPTIONS:
                return true;
            // 查询
            case GET:
                return methodGetHandler(request, response);
            // 新增
            case POST:
                // 修改
                // case PUT:
                // 删除
                // case DELETE:
                return methodReqBodyHandler(request, response, handler);
            default:
                throw new ApplicationException(UnifyResultCode.METHOD_NO_SUPPORT_ERROR, I18NUtil.getMsg("method.no.support.error", reqMethod));
        }
    }

    private boolean methodReqBodyHandler(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String header = request.getHeader("Content-Type");
        String uploadContentType = "multipart/form-data";
        if (StringUtils.contains(StringUtils.lowerCase(header), uploadContentType.toLowerCase())) {
            log.debug("upload file");
            return true;
        }


        String requestBody = "";
        try {
            requestBody = IOUtils.toString(request.getReader());
        } catch (Exception ex) {
            if ("getInputStream() has already been called for this request".equals(ex.getMessage())) {
                return true;
            }
        }

        if (log.isDebugEnabled()) {
            log.debug("requestBody: {}", requestBody);
        }
        if (StringUtils.isBlank(requestBody)) {
            log.warn("The requestBody is blank.");
        }
        try {
            CryptoDTO cryptoDTO = JsonUtil.json2JavaBean(requestBody, CryptoDTO.class);

            // 判定必要参数是否存在
            String timestamp = cryptoDTO.getTimestamp();
            String nonce = cryptoDTO.getNonce();
            String signature = cryptoDTO.getSignature();
            String appkey = cryptoDTO.getAppkey();
            if (StringUtils.isAnyBlank(timestamp, nonce, signature)) {
                // HTTP请求不合法，请求参数可能被篡改
                // return false;
                throw new ApplicationException(UnifyResultCode.METHOD_SIGN_ERROR, I18NUtil.getMsg("method.sign.error"));
            }

            // 向请求中绑定数据 => 用于尝试重试
            request.setAttribute(CommonConstant.HTTP_REQ_ATTR_APPKEY, appkey);
            request.setAttribute(CommonConstant.HTTP_REQ_ATTR_NONCE, nonce);
            request.setAttribute(CommonConstant.HTTP_REQ_ATTR_SIGNATURE, signature);
            request.setAttribute(CommonConstant.HTTP_REQ_ATTR_TIMESTAMP, timestamp);

            // nonce 唯一性判定
            checkNonce(nonce, request);

            // 判定时间戳是否在允许的范围内
            checkTimestampTimeout(timestamp);

            // 未加密 或者 参数不正确
            if (StringUtils.isAnyBlank(cryptoDTO.getEndata(), cryptoDTO.getEnkey())) {
                Map<String, String> pMap = parseJsonTextForSignature(requestBody);
                if (!SignUtil.checkSign(signature, pMap, PARAM_SIGNATURE)) {
                    //  HTTP请求不合法，请求参数可能被篡改 [签名不合法]
                    // return false;
                    throw new ApplicationException(UnifyResultCode.METHOD_SIGN_ERROR, I18NUtil.getMsg("method.sign.error"));
                }
                return true;
            }

            // 已加密
            // boolean useGm = datalkCryptoCfgs.useGm(RequestHeaderUtil.getXClientTypeStr(request), RequestHeaderUtil.getXClientVer(request));
            // AppSecurityInfo appSecurityInfo = appEncryptSecurityKeyComponent.getByAppKey(appkey);
            // String serverPrivateKey;
            // if (appSecurityInfo != null) {
            //     serverPrivateKey = appSecurityInfo.getServerPrivateKey();
            // } else {
            //     serverPrivateKey = useGm ? datalkCryptoCfgs.getGmServer().getPrivateKey() : datalkCryptoCfgs.getServer().getPrivateKey();
            // }
            // if (StringUtils.isBlank(serverPrivateKey)) {
            //     log.warn("serverPrivateKey is blank. [appkey={}, useGm={}]", appkey, useGm);
            // }
            //
            // String decrypt;
            // if (useGm) {
            //     String sm4Key = GMUtils.sm2Decrypt(serverPrivateKey, cryptoDTO.getEnkey());
            //     decrypt = GMUtils.sm4Decrypt(cryptoDTO.getEndata(), sm4Key);
            // } else {
            //     String aseKey = RSATool.decrypt(cryptoDTO.getEnkey(), serverPrivateKey);
            //     decrypt = AESTool.decrypt(cryptoDTO.getEndata(), aseKey);
            // }
            // // String aseKey = RSATool.decrypt(cryptoDTO.getEnkey(), datalkCryptoCfgs.getServer().getPrivateKey().replaceAll("\\s", ""));
            // // String decrypt = AESTool.decrypt(cryptoDTO.getEndata(), aseKey);
            // decrypt = StringUtils.isBlank(decrypt) ? "{}" : decrypt;
            boolean useGm = this.useGm(request);
            String decrypt = this.decrypt(cryptoDTO, useGm);
            if (log.isDebugEnabled()) {
                log.debug("[useGm={}, decrypt={}]", useGm, decrypt);
            }
            // Map<String, String> pMap = parseJsonTextForSignature(decrypt);
            // pMap = MapUtils.isEmpty(pMap) ? new HashMap<>(4) : pMap;
            // pMap.put("nonce", nonce);
            // pMap.put("timestamp", timestamp);
            // pMap.put("appkey", appkey);
            // boolean isVerifySignature;
            // if (useGm) {
            //     String preSignStr = SignUtil.genSignStr(SignUtil.sortMap(pMap), PARAM_SIGNATURE);
            //     isVerifySignature = GMUtils.verifySm3(preSignStr, signature);
            // } else {
            //     isVerifySignature = SignUtil.checkSign(signature, pMap, PARAM_SIGNATURE);
            // }
            // if (!isVerifySignature) {
            //     throw new ApplicationException(UnifyResultCode.METHOD_SIGN_ERROR, I18NUtil.getMsg("method.sign.error"));
            // }
            this.checkSignature(decrypt, useGm, appkey, nonce, timestamp, signature);
            // if (!SignUtil.checkSign(signature, pMap, "signature")) {
            //     //  HTTP请求不合法，请求参数可能被篡改 [签名不合法]
            //     // return false;
            //     throw new ApplicationException(UnifyResultCode.METHOD_SIGN_ERROR, I18NUtil.getMsg("method.sign.error"));
            // }

            // // 记录请求，并设置超时时间
            // redisUtil.set(reqSignNoncePrefix.concat(nonce), nonce, reqSignNonceTimeoutDefaultSecond);
            return true;
        } catch (Exception ex) {
            log.warn("{}", ex.getMessage());
            if (ex instanceof ApplicationException) {
                throw ex;
            }
            throw new ApplicationException(UnifyResultCode.METHOD_SIGN_ERROR, I18NUtil.getMsg("method.sign.error"));
        }
    }

    public void checkSignature(String decrypt, boolean useGm, String appkey, String nonce, String timestamp, String signature) {

        Map<String, String> pMap = parseJsonTextForSignature(decrypt);
        pMap = MapUtils.isEmpty(pMap) ? new HashMap<>(4) : pMap;
        pMap.put("nonce", nonce);
        pMap.put("timestamp", timestamp);
        Boolean appKeySign = datalkCryptoCfgs.getNeedAppkeySign();
        if (appKeySign) {
            pMap.put("appkey", appkey);
        }
        boolean isVerifySignature;
        if (useGm) {
            String preSignStr = SignUtil.genSignStr(SignUtil.sortMap(pMap), PARAM_SIGNATURE);
            isVerifySignature = GMUtils.verifySm3(preSignStr, signature);
        } else {
            isVerifySignature = SignUtil.checkSign(signature, pMap, PARAM_SIGNATURE);
        }
        if (!isVerifySignature) {
            throw new ApplicationException(UnifyResultCode.METHOD_SIGN_ERROR, I18NUtil.getMsg("method.sign.error"));
        }

        // 记录请求，并设置超时时间
        redisUtil.set(reqSignNoncePrefix.concat(nonce), nonce, reqSignNonceTimeoutDefaultSecond);
    }

    public boolean useGm(HttpServletRequest request) {
        boolean useGm = datalkCryptoCfgs.useGm(RequestHeaderUtil.getXClientTypeStr(request), RequestHeaderUtil.getXClientVer(request));
        return useGm;
    }

    public String decrypt(CryptoDTO cryptoDTO, boolean useGm) {
        String appkey = cryptoDTO.getAppkey();
        AppSecurityInfo appSecurityInfo = appEncryptSecurityKeyComponent.getByAppKey(appkey);
        String serverPrivateKey;
        if (appSecurityInfo != null) {
            serverPrivateKey = appSecurityInfo.getServerPrivateKey();
        } else {
            serverPrivateKey = useGm ? datalkCryptoCfgs.getGmServer().getPrivateKey() : datalkCryptoCfgs.getServer().getPrivateKey();
        }
        if (StringUtils.isBlank(serverPrivateKey)) {
            log.warn("serverPrivateKey is blank. [appkey={}, useGm={}]", appkey, useGm);
        }

        String decrypt;
        if (useGm) {
            String sm4Key = GMUtils.sm2Decrypt(serverPrivateKey, cryptoDTO.getEnkey());
            decrypt = GMUtils.sm4Decrypt(cryptoDTO.getEndata(), sm4Key);
        } else {
            String aseKey = RSATool.decrypt(cryptoDTO.getEnkey(), serverPrivateKey);
            decrypt = AESTool.decrypt(cryptoDTO.getEndata(), aseKey);
        }
        // String aseKey = RSATool.decrypt(cryptoDTO.getEnkey(), datalkCryptoCfgs.getServer().getPrivateKey().replaceAll("\\s", ""));
        // String decrypt = AESTool.decrypt(cryptoDTO.getEndata(), aseKey);
        decrypt = StringUtils.isBlank(decrypt) ? "{}" : decrypt;
        return decrypt;
    }

    public Map<String, String> encrypt(Object obj, String appkey, HttpServletRequest request) {
        UnifyResult unifyResult = this.encryptV2(obj, appkey, request);
        Map dataMap = (Map) unifyResult.getData();
        String enkey = String.valueOf(dataMap.get("enkey"));
        String endata = String.valueOf(dataMap.get("endata"));
        Map<String, String> resMap = Maps.newHashMap();
        resMap.put("enkey", enkey);
        resMap.put("endata", endata);
        return resMap;

    }

    public UnifyResult encryptV2(Object obj, String appkey, HttpServletRequest request) {
        boolean useGm = this.useGm(request);
        String aseKey = useGm ? EncryptResponseBodyAdvice.genRandomHex(32) : EncryptResponseBodyAdvice.getRandomString(16);
        AppSecurityInfo appSecurityInfo = appEncryptSecurityKeyComponent.getByAppKey(appkey);
        String clientPublicKey;
        if (appSecurityInfo != null) {
            clientPublicKey = appSecurityInfo.getClientPublicKey();
        } else {
            clientPublicKey = useGm ? datalkCryptoCfgs.getGmClient().getPublicKey() : datalkCryptoCfgs.getClient().getPublicKey();
        }
        String enkey = useGm //
                ? GMUtils.sm2Encrypt(clientPublicKey, aseKey)  //
                : RSATool.encrypt(aseKey, clientPublicKey);

        String endata = "";
        UnifyResult res;
        if (obj instanceof String) {
            res = UnifyResult.success();
            endata = useGm ? GMUtils.sm4Encrypt((String) obj, aseKey) : AESTool.encrypt((String) obj, aseKey);
        } else if (obj instanceof UnifyResult) {
            res = (UnifyResult) obj;
            Object data = res.getData();
            if (data == null) {
                return res;
            }
            String preEncryptStr = "";
            if (data instanceof String) {
                preEncryptStr = (String) data;
            } else {
                preEncryptStr = JsonUtil.getJsonFromObject(data);
            }
            endata = useGm ? GMUtils.sm4Encrypt(preEncryptStr, aseKey) : AESTool.encrypt(preEncryptStr, aseKey);
        } else {
            res = UnifyResult.success();
            String json = JsonUtil.getJsonFromObject(obj);
            endata = useGm ? GMUtils.sm4Encrypt(json, aseKey) : AESTool.encrypt(json, aseKey);
        }

        return UnifyResult.create(res.getRsCode(), res.getRsCause()).mapData().put("endata", endata).put("enkey", enkey);

    }



    private boolean methodGetHandler(HttpServletRequest request, HttpServletResponse response) {
        // 排除 请求页面的GET方法
        String xAccessToken = request.getHeader(CommonConstant.ACCESS_TOKEN);
        if (xAccessToken == null) {
            return true;
        }
        String nonce = request.getParameter("nonce");
        String timestamp = request.getParameter("timestamp");
        String signature = request.getParameter("signature");
        String appkey = request.getParameter("appkey");
        if (StringUtils.isAnyBlank(timestamp, nonce, signature)) {
            // HTTP请求不合法，请求参数可能被篡改
            // return false;
            throw new ApplicationException(UnifyResultCode.METHOD_SIGN_ERROR, I18NUtil.getMsg("method.sign.error"));
        }

        // 向请求中绑定数据 => 用于尝试重试
        request.setAttribute(CommonConstant.HTTP_REQ_ATTR_APPKEY, appkey);
        request.setAttribute(CommonConstant.HTTP_REQ_ATTR_NONCE, nonce);
        request.setAttribute(CommonConstant.HTTP_REQ_ATTR_SIGNATURE, signature);
        request.setAttribute(CommonConstant.HTTP_REQ_ATTR_TIMESTAMP, timestamp);

        // nonce 唯一性判定
        checkNonce(nonce, request);

        // 判定时间戳是否在允许的范围内
        checkTimestampTimeout(timestamp);

        Map<String, String> paramsMap = new HashMap<>(8);
        Enumeration<String> paramNames = request.getParameterNames();
        while (paramNames.hasMoreElements()) {
            String name = paramNames.nextElement();
            // if (!StringUtils.equalsAny(name, "timestamp", "nonce", "signature")) {
            if (StringUtils.equalsAny(name, PARAM_SIGNATURE)) {
                continue;
            }
            String value = request.getParameter(name);
            paramsMap.put(name, value);
        }


        if (!SignUtil.checkSign(signature, paramsMap, PARAM_SIGNATURE)) {
            throw new ApplicationException(UnifyResultCode.METHOD_SIGN_ERROR, I18NUtil.getMsg("method.sign.error"));
        }

        // 记录请求，并设置超时时间
        redisUtil.set(reqSignNoncePrefix.concat(nonce), nonce, reqSignNonceTimeoutDefaultSecond);

        return true;
    }

    public void checkTimestampTimeout(String timestamp) {
        String timestampTimeoutStr = redisUtil.getStr(this.reqSignTimestampTimeoutKey);
        long timestampTimeout = BasicParserUtils.parseLong(timestampTimeoutStr, this.reqSignTimestampTimeoutDefaultSecond);
        long timestampLong = BasicParserUtils.parseLong(timestamp, 0L);
        if (System.currentTimeMillis() - timestampLong >= timestampTimeout * 1000) {
            // HTTP请求不合法，请求超时
            // return false;
            throw new ApplicationException(UnifyResultCode.METHOD_SIGN_ERROR, I18NUtil.getMsg("method.sign.error"));
        }
    }

    public void checkNonce(String nonce, HttpServletRequest request) {
        Object nonceObj = redisUtil.get(reqSignNoncePrefix.concat(nonce));
        if (nonceObj == null) {
            return;
        }

        // 重试
        String nonceStr = String.valueOf(nonceObj);
        if (nonce.equals(nonceStr)) {
            String errorRetryKey = CommonConstant.HTTP_NONCE_ERROR_RETRY_PREFIX.concat(nonce);
            String skipNonceCheck = (String) request.getAttribute(errorRetryKey);
            if (!nonce.equals(skipNonceCheck)) {
                throw new ApplicationException(UnifyResultCode.METHOD_SIGN_ERROR, I18NUtil.getMsg("method.nonce.error"));
            }

            long incr = redisUtil.incr(errorRetryKey, 1);
            redisUtil.expire(errorRetryKey, reqSignNonceTimeoutDefaultSecond);
            if (incr > 3) {
                throw new ApplicationException(UnifyResultCode.METHOD_SIGN_ERROR, I18NUtil.getMsg("method.nonce.error"));
            }
            return;
        }

        throw new ApplicationException(UnifyResultCode.METHOD_SIGN_ERROR, I18NUtil.getMsg("method.nonce.error"));
    }

    /**
     * 将JSON文本解析成签名时使用的数据Map
     *
     * @param jsonText JSON文本，必须是一个JSON对象的文本形式
     *
     * @return {@code Map<String,String>} 数据Map
     *
     * @author ZhouYi
     * @date 2020/5/19 18:07
     * @description description
     * @note 该方法的解析规则如下：
     * <ul>
     *     <li>基础数据类型（文本、数字、字符）：调用{@link JsonElement#getAsString()}获取文本值</li>
     *     <li>其他数据类型（null、数组、对象）：直接调用{@link JsonElement#toString()}，按照GSON规则转化为文本</li>
     * </ul>
     */
    public static Map<String, String> parseJsonTextForSignature(String jsonText) {
        // STEP Number 声明解析器，解析根元素
        JsonParser parser = new JsonParser();
        // JsonElement rootElement = parser.parse(jsonText);
        JsonElement rootElement = JsonParser.parseString(jsonText);

        // STEP Number 校验根元素类型
        if ((rootElement == null) || !rootElement.isJsonObject()) {
            throw new RuntimeException("解析JSON文本失败：目标JSON元素必须是一个对象");
        }

        // STEP Number 获取根元素的所有一级属性，循环处理这些属性
        Map<String, String> jsonDataMap = new HashMap<String, String>(); // JSON数据Map，该方法的返回值
        for (Map.Entry<String, JsonElement> dataEntry : rootElement.getAsJsonObject().entrySet()) {
            // SUBSTEP Number 读取属性的文本Key和元素Value
            String key = dataEntry.getKey();
            JsonElement valueElement = dataEntry.getValue();

            // SUBSTEP Number 将元素Value按照指定的规则转化为文本
            String valueText = null;
            if (valueElement.isJsonPrimitive()) {
                valueText = valueElement.getAsString();

            } else {
                valueText = valueElement.toString();

            }

            // SUBSTEP Number 将处理好的Key-Value放入Map中
            jsonDataMap.put(key, valueText);

        }

        // STEP Number 返回解析完毕的JSON数据Map
        return jsonDataMap;

    }

    public static void main(String[] args) {
        try {
            String reqMethod = null;
            System.out.println(HttpMethod.GET.matches(reqMethod));
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

}
